using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Diagnostics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;

namespace Oops.Xna.Framework.Content.Pipeline
{
	[ContentImporter(".xna_model", DefaultProcessor = "Model - Oops! XNA Framework", DisplayName = "Oops! XNA Model File")]
    public class XnaModelImporter : ContentImporter<NodeContent>
	{
		private XmlDocument document;
        private NodeContent rootContent;

		private Dictionary<int, int> indices = new Dictionary<int, int>();
		private Dictionary<int, NodeContent> bones = new Dictionary<int, NodeContent>();
		private Dictionary<int, string> diffuseTextures = new Dictionary<int, string>();
		private Dictionary<int, MaterialContent> materials = new Dictionary<int, MaterialContent>();

        public override NodeContent Import(String filename, ContentImporterContext context)
		{
			//System.Diagnostics.Debugger.Launch();

			// The context parameter must be specified.
			if (context == null)
			{
				// TODO :
				throw new PipelineException("");
			}

			// Load the file into the xml document.
			this.document = new XmlDocument();
			this.document.Load(filename);
			
			// Create the root content instance.
            this.rootContent = new NodeContent();

			// Import the sub-content.
			this.ImportBones(context);
			this.ImportTextures(context);
			this.ImportMaterials(context);
			this.ImportMeshes(context);

			// Return the model content.
			return this.rootContent;
		}

		private void ImportBones(ContentImporterContext context)
		{
			// Assert parameters.
			Debug.Assert(context != null);

			// Get the root bone information for the xml document.
			XmlNode xmlRoot = this.document.SelectSingleNode(".//bone[@id=0]");

			// Import the root content.
			this.bones.Clear();
			this.rootContent = this.ImportBone(xmlRoot, context);
		}

		private NodeContent ImportBone(XmlNode boneXml, ContentImporterContext context)
		{
			// Assert parameters.
			Debug.Assert(boneXml != null);
			Debug.Assert(context != null);

			// Get the id of the current bone.
			int id = XmlConvert.ToInt32(boneXml.Attributes["id"].Value);

			// Get the parent id of the current bone.
			int parentId = XmlConvert.ToInt32(boneXml.Attributes["parentId"].Value);

			// Get the name of the current bone.
			string name = boneXml.Attributes["name"].Value;
			
			// Get the position of the current bone.
			Vector3 position;
			position.X = XmlConvert.ToSingle(boneXml.Attributes["x"].Value);
			position.Y = XmlConvert.ToSingle(boneXml.Attributes["y"].Value);
			position.Z = XmlConvert.ToSingle(boneXml.Attributes["z"].Value);
	
			// Get the orientation of the current bone.
			Quaternion orientation;
			orientation.X = XmlConvert.ToSingle(boneXml.Attributes["orientX"].Value);
			orientation.Y = XmlConvert.ToSingle(boneXml.Attributes["orientY"].Value);
			orientation.Z = XmlConvert.ToSingle(boneXml.Attributes["orientZ"].Value);
			orientation.W = XmlConvert.ToSingle(boneXml.Attributes["orientW"].Value);

			// Create a new bone content from the information in the current iteration.
			NodeContent boneContent = parentId == -1 ? this.rootContent : new BoneContent();
			boneContent.Name = name;
			boneContent.Transform = Matrix.CreateFromQuaternion(orientation) * Matrix.CreateTranslation(position);
			boneContent.OpaqueData["Position"] = position;
			boneContent.OpaqueData["Orientation"] = orientation;
			
			// Iterate on found child bone definitions.
			foreach (XmlNode childBoneXml in this.document.SelectNodes(String.Format(".//bone[@parentId={0}]", id)))
			{
				boneContent.Children.Add(this.ImportBone(childBoneXml, context));
			}

			// Add the bone to private list and return the content.
			this.bones.Add(id, boneContent);
			return boneContent;
		}
		
		private void ImportTextures(ContentImporterContext context)
		{
			// Assert parameters.
			Debug.Assert(context != null);
			
			// Iterate on found texture definitions.
			this.diffuseTextures.Clear();
			foreach (XmlNode textureXml in this.document.SelectNodes(".//texture"))
			{
				// Get the id of the current texture.
				int id = XmlConvert.ToInt32(textureXml.Attributes["id"].Value);

				// Get the path of the current texture.
				string path = textureXml.Attributes["path"].Value;

				// Add the texture to the private list.
				this.diffuseTextures.Add(id, path);
			}
		}

		private void ImportMaterials(ContentImporterContext context)
		{
			// Assert parameters.
			Debug.Assert(context != null);

			// Iterate on found material definitions.
			this.materials.Clear();
			foreach (XmlNode materialXml in this.document.SelectNodes(".//material"))
			{
				// Get the id of the current material.
				int id = XmlConvert.ToInt32(materialXml.Attributes["id"].Value);

				// Get the diffuse texture id of the current material.
				// TODO : int diffuseTextureId = XmlConvert.ToInt32(materialXml.Attributes["diffuseTextureId"].Value);

				// Get the diffuse color of the current material.
				Vector3 diffuseColor;
				diffuseColor.X = XmlConvert.ToSingle(materialXml["diffuse"].Attributes["red"].Value);
				diffuseColor.Y = XmlConvert.ToSingle(materialXml["diffuse"].Attributes["green"].Value);
				diffuseColor.Z = XmlConvert.ToSingle(materialXml["diffuse"].Attributes["blue"].Value);
				
				// Get the specular color of the current material.
				Vector3 specularColor;
				specularColor.X = XmlConvert.ToSingle(materialXml["specular"].Attributes["red"].Value);
				specularColor.Y = XmlConvert.ToSingle(materialXml["specular"].Attributes["green"].Value);
				specularColor.Z = XmlConvert.ToSingle(materialXml["specular"].Attributes["blue"].Value);
				float specularPower = XmlConvert.ToSingle(materialXml["specular"].Attributes["intensity"].Value);
				
				// Create a new material content from the information in the current iteration.
				//XnamlMaterialContent materialContent = new XnamlMaterialContent();
				BasicMaterialContent materialContent = new BasicMaterialContent();
				materialContent.DiffuseColor = diffuseColor;
				materialContent.SpecularColor = specularColor;
				materialContent.SpecularPower = specularPower;
				materialContent.VertexColorEnabled = true;
				//materialContent.Texture = new ExternalReference<Texture2DContent>(filename);
				//materialContent.Effect = new ExternalReference<EffectContent>(@".\Content\Effects\XnamlEffect.fx");
				
				// Add the material to private list.
				this.materials.Add(id, materialContent);
			}
		}

		private void ImportMeshes(ContentImporterContext context)
		{
			// Assert parameters.
			Debug.Assert(context != null);

			// Iterate on found mesh definitions.
			foreach (XmlNode meshXml in this.document.SelectNodes(".//mesh"))
			{
				// Get the id of the current mesh.
				int id = XmlConvert.ToInt32(meshXml.Attributes["id"].Value);

				// Get the material id of the current mesh.
				int materialId = XmlConvert.ToInt32(meshXml.Attributes["materialId"].Value);

				// Start building the mesh.
				MeshBuilder meshBuilder = MeshBuilder.StartMesh("~" + id.ToString());
				meshBuilder.MergeDuplicatePositions = false;
				
				// Import the vertices and indices of the current mesh definition.
				this.ImportVertices(meshBuilder, meshXml, context);
				this.ImportIndices(meshBuilder, meshXml, context);

				// Optimize the mesh content
				MeshContent meshContent = meshBuilder.FinishMesh();
				MeshHelper.OptimizeForCache(meshContent);
				
				// Add the mesh content to the root content.
				this.rootContent.Children.Add(meshContent);
			}
		}

		private void ImportVertices(MeshBuilder meshBuilder, XmlNode meshXml, ContentImporterContext context)
		{
			// Assert parameters.
			Debug.Assert(meshBuilder != null);
			Debug.Assert(meshXml != null);
			Debug.Assert(context != null);

			// Iterate on all vertex definitions of the current mesh.
			this.indices.Clear();
			foreach (XmlElement vertexXml in meshXml.SelectNodes(".//vertex"))
			{
				// Get the id of the vertex.
				int id = XmlConvert.ToInt32(vertexXml.Attributes["id"].Value);

				// Get the position of the vertex.
				Vector3 position;
				position.X = XmlConvert.ToSingle(vertexXml.Attributes["x"].Value);
				position.Y = XmlConvert.ToSingle(vertexXml.Attributes["y"].Value);
				position.Z = XmlConvert.ToSingle(vertexXml.Attributes["z"].Value);

				// Get the start index and range of the weights for the current vertex.
				int weightId = XmlConvert.ToInt32(vertexXml.Attributes["weightId"].Value);
				int weightCount = XmlConvert.ToInt32(vertexXml.Attributes["weightCount"].Value);
				
				// Iterate on all the weight indices for the current vertex.
				for (int weightIndex = weightId; weightIndex < weightId + weightCount; weightIndex++)
				{
					// Get the current weight definition for the vertex.
					XmlNode weightXml = meshXml.SelectSingleNode(String.Format(".//weight[@id={0}]", weightIndex));

					// Get the current weight's associated bone.
					int boneId = XmlConvert.ToInt32(weightXml.Attributes["boneId"].Value);
					NodeContent bone = this.bones[boneId];

					// Get the influence of the bone on the position of the vertex and apply it to the position.
					float influence = XmlConvert.ToSingle(weightXml.Attributes["influence"].Value);
					
					// Modify vertex position to make it bone relative.
					position = Vector3.Transform(position, Matrix.Invert(bone.AbsoluteTransform / influence));
				}

				// Create the vertex and store the index in the mapping collection.
				int index = meshBuilder.CreatePosition(position);
				this.indices.Add(id, index);
			}
		}

		private void ImportIndices(MeshBuilder meshBuilder, XmlNode meshXml, ContentImporterContext context)
		{
			// Assert parameters.
			Debug.Assert(meshBuilder != null);
			Debug.Assert(meshXml != null);
			Debug.Assert(context != null);

			// Hold onto material id.
			int materialId = XmlConvert.ToInt32(meshXml.Attributes["materialId"].Value);

			// Define vertex channels.
			int normalChannelIndex = meshBuilder.CreateVertexChannel<Vector3>(VertexChannelNames.Normal(0));
			int colorChannelIndex = meshBuilder.CreateVertexChannel<Color>(VertexChannelNames.Color(0));
			int textureCoordinatesIndex = meshBuilder.CreateVertexChannel<Vector2>(VertexChannelNames.TextureCoordinate(0));
			int weightChannelIndex = meshBuilder.CreateVertexChannel<BoneWeightCollection>(VertexChannelNames.Weights(0));

			// Iterate on all triangle definitions of the current mesh.
			Dictionary<int, Vector3> vertexNormals = new Dictionary<int, Vector3>();
			Dictionary<int, Vector2> vertexTextureCoordinates = new Dictionary<int, Vector2>();
			Dictionary<int, BoneWeightCollection> vertexBoneWeights = new Dictionary<int, BoneWeightCollection>();

			foreach (XmlNode triangleXml in meshXml.SelectNodes(".//triangle"))
			{
				// Iterate 3 times for each vertex of the current triangle.
				for (int triangleIndex = 1; triangleIndex <= 3; triangleIndex++)
				{
					// Get the current vertex id.
					int vertexId = XmlConvert.ToInt32(triangleXml.Attributes[String.Format("vertexId{0}", triangleIndex)].Value);

					// Get the vertex from the id.
					XmlNode vertexXml = meshXml.SelectSingleNode(String.Format(".//vertex[@id={0}]", vertexId));

					// Get the normal of the vertex.
					Vector3 normal;
					normal.X = XmlConvert.ToSingle(vertexXml.Attributes["normX"].Value);
					normal.Y = XmlConvert.ToSingle(vertexXml.Attributes["normY"].Value);
					normal.Z = XmlConvert.ToSingle(vertexXml.Attributes["normZ"].Value);

					// Get the texture coordinates.
					Vector2 textureCoordinates;
					//textureCoordinates.X = XmlConvert.ToSingle(vertexXml.Attributes["u"].Value);
					//textureCoordinates.Y = XmlConvert.ToSingle(vertexXml.Attributes["v"].Value);
					textureCoordinates.X = XmlConvert.ToSingle(triangleXml.Attributes[String.Format("u{0}", triangleIndex)].Value);
					textureCoordinates.Y = XmlConvert.ToSingle(triangleXml.Attributes[String.Format("v{0}", triangleIndex)].Value);
					
					// Get the weights of the vertex.
					BoneWeightCollection weights;
					if (vertexBoneWeights.ContainsKey(vertexId))
					{
						weights = vertexBoneWeights[vertexId];

						// Get the start index and range of the weights for the current vertex.
						int weightId = XmlConvert.ToInt32(vertexXml.Attributes["weightId"].Value);
						int weightCount = XmlConvert.ToInt32(vertexXml.Attributes["weightCount"].Value);

						// Get the all the weight indices for the current vertex.
						for (int weightIndex = weightId; weightIndex < weightId + weightCount; weightIndex++)
						{
							// Get the current weight definition for the vertex.
							XmlNode weightXml = meshXml.SelectSingleNode(String.Format(".//weight[@id={0}]", weightIndex));

							// Get the current weight's associated bone.
							int boneId = XmlConvert.ToInt32(weightXml.Attributes["boneId"].Value);
							NodeContent bone = this.bones[boneId];

							// Get the influence of the bone on the position of the vertex.
							float influence = XmlConvert.ToSingle(weightXml.Attributes["influence"].Value);

							// Update normal by the bone's orientation.
							normal = Vector3.TransformNormal(normal, Matrix.Invert(bone.AbsoluteTransform));
							normal.Normalize();
						}
					}
					else
					{
						// Get the start index and range of the weights for the current vertex.
						int weightId = XmlConvert.ToInt32(vertexXml.Attributes["weightId"].Value);
						int weightCount = XmlConvert.ToInt32(vertexXml.Attributes["weightCount"].Value);

						// Get the all the weight indices for the current vertex.
						weights = new BoneWeightCollection();
						for (int weightIndex = weightId; weightIndex < weightId + weightCount; weightIndex++)
						{
							// Get the current weight definition for the vertex.
							XmlNode weightXml = meshXml.SelectSingleNode(String.Format(".//weight[@id={0}]", weightIndex));

							// Get the current weight's associated bone.
							int boneId = XmlConvert.ToInt32(weightXml.Attributes["boneId"].Value);
							NodeContent bone = this.bones[boneId];

							// Get the influence of the bone on the position of the vertex.
							float influence = XmlConvert.ToSingle(weightXml.Attributes["influence"].Value);

							// Apply weight.
							weights.Add(new BoneWeight(bone.Name, influence));

							// Update normal by the bone's orientation.
							normal = Vector3.TransformNormal(normal, Matrix.Invert(bone.AbsoluteTransform));
							normal.Normalize();
						}
						weights.NormalizeWeights(4);

						// Cache the bone weight collection.
						vertexBoneWeights.Add(vertexId, weights);
					}

					// Apply mesh vertex attributes.
					meshBuilder.SetVertexChannelData(normalChannelIndex, normal);
					meshBuilder.SetVertexChannelData(colorChannelIndex, new Color(((BasicMaterialContent)this.materials[materialId]).DiffuseColor.Value));
					meshBuilder.SetVertexChannelData(textureCoordinatesIndex, textureCoordinates);
					meshBuilder.SetVertexChannelData(weightChannelIndex, weights);
					
					meshBuilder.SetMaterial(this.materials[materialId]);				

					// Add the vertex index to the index buffer.
					meshBuilder.AddTriangleVertex(this.indices[vertexId]);
				}
			}
		}
	}
}
